Recoil で Pagination する方法を考える
次にコールする時のページ番号またはカーソルを View が知っていて、 selectorFamily にパラメータとして渡す
const itemsInThisPage = useRecoilValue(paginatedItems(page));
code:paginate1.js
const paginatedItems = selectorFamily({
key: 'paginatedItems',
get: page => async () => {
return await fetchItems(/items?page=${page});
}
});
この方法だと取得だけなら簡単だが更新が難しい
まず、データの実体 (=Atom) がないので、更新するには reset して get し直すしかない 特定のデータを更新するためにページを丸ごと取得し直す必要があり、効率が悪い
ペジネーション以外でデータにアクセスしている場合、データの一意性が守られない id から取得した場合と page から取得した場合で同じデータが返ってくることを保証できない
データが一貫して固定なら問題は起こらないが、そうでなければ不整合が起こる
page に含まれるデータの id だけを取得して、さらに id => Data でデータ取得する
code:paginate2.js
const items = selectorFamily({
key: 'items',
get: id => async () => {
const result = await fetchItem(/items/${id});
return result;
}
});
const paginatedItems = selectorFamily({
key: 'paginatedItems',
get: page => async ({ get }) => {
const result = await fetchItems(/items?page=${page});
return result.map(item => get(items(item.id)));
}
});
データの一意性は保たれるようになった
fetchItems と fetchItem で同じデータを2回取得しているのは効率が悪い
元々 id だけをネットワークから取得しているのであれば、これで問題ない
fetchItems がローカルにデータをキャッシュして、 fetchItem でそれを取得できれば良いのでは?
そのローカルキャッシュは、誰がいつどうやってクリアするのか?
items を reset しても fetchItem がキャッシュを返しては意味がない
(余談)キャッシュから取得するかネットワークから取得するかをパラメータに含むことはできない
異なるパラメータでも同じ Selector だと Recoil に誤認させることはできるが、流石にハックが過ぎる teramotodaiki.icon
これ公式で出来るようにならないかな。 get(Parameter, Dependency) みたいに分ける感じ teramotodaiki.icon
キャッシュの参照は Selector の「外」になければならない
一時的なキャッシュを Selector の「外」に持つ
code:paginate2.js
const itemsTempCache = new Map(); // 一時的なキャッシュ
const items = selectorFamily({
key: 'items',
get: id => async () => {
const result = itemsTempCache.get(id) || await fetchItem(/items/${id});
return result;
}
});
const paginatedItems = selectorFamily({
key: 'paginatedItems',
get: page => async ({ get }) => {
const result = await fetchItems(/items?page=${page});
result.forEach(item => itemsTempCache.set(item.id, item)); // キャッシュに入れる
const items = result.map(item => get(items(item.id))); // キャッシュを利用する
result.forEach(item => itemsTempCache.delete(item.id)); // キャッシュをクリアする
return items;
}
});
同じデータを2回取得することがなくなった
ただしこの実装には2つの暗黙の前提がある
Selector の get が同期的に実行されるという前提
試していないので、これが成り立つかどうか分からない teramotodaiki.icon
Promised かどうかはコールするまで Recoil にも分からないので、多分同期的に呼ばれると思うteramotodaiki.icon
items の get が paginatedItems を get しないという前提
言い換えれば「循環参照がない」という前提
この前提が崩れた場合、キャッシュをクリアできなくなってしまう
これは Recoil が例外をスローしてくれるので、守られる
図にするとこんな感じ?
https://gyazo.com/90321c1e3e6aaf062c7d0e26b413752f
水色の丸が Selector, 水色の四角が selectorFamily を表している
細かい話
ページが読み込まれた後に作成されたドキュメント(カーソル位置が先頭よりも若いデータ)をどう取得する?
「初回取得時のカーソルより若いものを、カーソルが古い順に Subscribe する Selector」を作れる場合
つまり Firestore の Query onSnapshot のようなものがある場合
Observable -> Selector が実装できるならば、これも ReadOnlySelector で実装できる
ペジネーションとは別に、こちらも View で useRecoilValue すれば良い
ユーザーが新しいデータを追加した場合
つまり新しいデータがローカルからサーバへ流れていく場合
ReadWriteSelector でこんな感じに実装するしかなさそう
code:add.js
const myNewItems = atom({
key: 'myNewItems',
default: []
})
function Component() {
const setter = useSetRecoilState(myNewItems);
const add = useCallback(addedItem => {
setter(prevItems => prevItems.concat(addedItem));
});
return ...
}